/*
 * Created on Oct 27, 2004
 */
package no.ntnu.fp.net.co;

import java.io.EOFException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import no.ntnu.fp.net.cl.ClSocket;
import no.ntnu.fp.net.cl.KtnDatagram;
import no.ntnu.fp.net.cl.KtnDatagram.Flag;

/**
 * Implementation of the Connection-interface. <br>
 * <br>
 * This class implements the behaviour in the methods specified in the interface
 * {@link Connection} over the unreliable, connectionless network realised in
 * {@link ClSocket}. The base class, {@link AbstractConnection} implements some
 * of the functionality, leaving message passing and error handling to this
 * implementation.
 * 
 * @author Sebj�rn Birkeland and Stein Jakob Nordb�
 * @see no.ntnu.fp.net.co.Connection
 * @see no.ntnu.fp.net.cl.ClSocket
 */
public class ConnectionImpl extends AbstractConnection {

    /** Keeps track of the used ports for each server port. */
    private static Map<Integer, Boolean> usedPorts 
    		= Collections.synchronizedMap(new HashMap<Integer, Boolean>());
    private static String localAddress; // This can be used instead of getIpv4Address
    //This will be set from the networksettingsdialog
    //Will not implement it yet incase Eirik doesn't use the Calendar
    
    public static void setLocalAddress(String localAddress){
    	ConnectionImpl.localAddress = localAddress;
    }

    /**
     * Initialise initial sequence number and setup state machine.
     * 
     * @param myPort
     *            - the local port to associate with this connection
     */
    public ConnectionImpl(int myPort) {
    	super();
    	usedPorts.put(myPort, true);
    	this.myPort= myPort;
    	this.myAddress = getIPv4Address();
    }
    
    public static int randomPortNumber() {
        int temp = 0;
           while (temp < 1024 || usedPorts.containsKey(temp)) {
               temp = (int)((Math.random()*20000) + 1024);
        }
        usedPorts.put(temp, true);
        return temp;
    }
    
    private String getIPv4Address() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        }
        catch (UnknownHostException e) {
            return "127.0.0.1";
        }
    }

    /**
     * Establish a connection to a remote location.
     * 
     * @param remoteAddress
     *            - the remote IP-address to connect to
     * @param remotePort
     *            - the remote portnumber to connect to
     * @throws IOException
     *             If there's an I/O error.
     * @throws java.net.SocketTimeoutException
     *             If timeout expires before connection is completed.
     * @see Connection#connect(InetAddress, int)
     */
    public void connect(InetAddress remoteAddress, int remotePort) throws IOException, 
    					SocketTimeoutException {
    	if(state != State.CLOSED){
    		throw new IllegalStateException("Not in CLOSED state when trying to establish connection.");
    	}
    	//Set information regarding remote host
    	this.remoteAddress = remoteAddress.getHostAddress();
    	this.remotePort = remotePort;
    	/*
    	 * Internal packets construction,
    	 * three-way-handshake seen from the client/guy connecting
    	 */
    	KtnDatagram syn = constructInternalPacket(Flag.SYN);
    	KtnDatagram synAck = null;
        try {
            simplySendPacket(syn); //HANDSHAKE PART 1: SENDING SYN
            state = State.SYN_SENT;
            synAck = receiveAck(); //HANDSHAKE PART 2: RECEIVING SYNACK
        }catch (Exception e){
        	System.err.println(e);
        }
        if (veryValid(synAck)) {
            lastValidPacketReceived = synAck;
            this.remotePort = synAck.getSrc_port();
    	    try {
    	        sendAck(synAck, false); //HANDSHAKE PART 3: SENDING ACK
    	    }catch (IOException e) {
                throw new IOException("Could not send ACK - Handshake Part 3 - connect()");
            } 
        } else {
            throw new IOException("Could not connect; did not receive valid SYN_ACK");
        }
    	//Established
    	state = State.ESTABLISHED;
    	/*
    	 *   \o/ CONNECTION \o/
    	 */
    }
    
    /**
     * Listen for, and accept, incoming connections.
     * 
     * @return A new ConnectionImpl-object representing the new connection.
     * @see Connection#accept()
     */
    public Connection accept() throws IOException, SocketTimeoutException {
    	if (state != State.CLOSED)
            throw new IllegalStateException("Can't accept connections in this state.");
        state = State.LISTEN;
        // Receiving SYN
        KtnDatagram syn = null;
        while (!veryValid(syn)){
        	try {
                syn = receivePacket(true); //HANDSHAKE PART 1: RECEIVE SYN
            } catch (Exception e) { 
            	System.err.println(e);
            } 
        }
        // Creating new connection
        ConnectionImpl connection = new ConnectionImpl(randomPortNumber());
        connection.remoteAddress = syn.getSrc_addr();
        connection.remotePort = syn.getSrc_port();
        connection.state = State.SYN_RCVD;

        // Handshaking
        KtnDatagram ack = null;
        try {
            while (!connection.veryValid(ack)) 
            	try {
                connection.sendAck(syn, true); //HANDSHAKE PART 2: SENDING SYN_ACK
                ack = connection.receiveAck(); //HANDSHAKE PART 3: RECEIVE ACK
            	} catch (Exception e){
            		System.err.println(e);
            	}
        } catch (Exception e) {
            state = State.CLOSED;
            throw new IOException("Unable to connect");
        }
        connection.state = State.ESTABLISHED;
        connection.lastValidPacketReceived = ack;
        state = State.CLOSED;
        return connection;
    }

    /**
     * Send a message from the application.
     * 
     * @param msg
     *            - the String to be sent.
     * @throws ConnectException
     *             If no connection exists.
     * @throws IOException
     *             If no ACK was received.
     * @see AbstractConnection#sendDataPacketWithRetransmit(KtnDatagram)
     * @see no.ntnu.fp.net.co.Connection#send(String)
     */
    //Used inside sends innerclass, -1 karma for bad implementation
    
    public void send(String msg) throws ConnectException, IOException {
    	if (state != State.ESTABLISHED){
            throw new IllegalStateException("Not in ESTABLISHED state.");
    	}
    	
    	//If we lose the ACK on SYN_ACK, we're in deep shit: Backup-solution:
        boolean established = false;
    	if (lastValidPacketReceived.getFlag() == Flag.SYN_ACK) {
            while(!established){
            	try{
                    sendAck(lastValidPacketReceived, false);
                    established = true;
            	}catch (SocketException e){
            		System.err.println("Failed on backup-solution for sending lost ACK on SYN_ACK - "+e);
            		e.printStackTrace();
            	}
            }
        }
    	//
    	KtnDatagram datagram = constructDataPacket(msg), ack = null;
    	ack = sendDataPacketWithRetransmit(datagram);
		
        /*System.err.println(ack == null ? "ack is null" : "" + ack.getFlag() 
    		+ ack.getChecksum() + ack.calculateChecksum());*/
        if (!veryValid(ack)) {
        	//Failing to send packet, send Ack on lastValidPacketReceived to resend this packet
        	//myAddress, myPort, remoteAddress, remotePort and sequenceNo
        	System.err.println("Invalid packet, resending ack on lastValidPacketReceived.");
        	System.err.println("lvp: " + lastValidPacketReceived + "\nSeqNr" + 
					lastValidPacketReceived.getSeq_nr() + "\nDestAddr" + 
					lastValidPacketReceived.getDest_addr() + "\nDestPort" + 
					lastValidPacketReceived.getDest_port() + "\nSrcAddr" + 
					lastValidPacketReceived.getSrc_addr() + "\nSrcPort" + 
					lastValidPacketReceived.getSrc_port() + "\nRA: "+remoteAddress+"\nRP: "+remotePort
					+"\nChecksum"+(lastValidPacketReceived.getChecksum() 
					== lastValidPacketReceived.calculateChecksum()) + "\nIsNull?"+
					(lastValidPacketReceived!=null));
        	sendAck(lastValidPacketReceived, false);
        }
        
        //Preventing SYN_ACK / ACK fix to run forever
        //Only runs if the SYN_ACK / ACK fix has been run
        if(established)lastValidPacketReceived = ack;
    }

    /**
     * Wait for incoming data.
     * 
     * @return The received data's payload as a String.
     * @see Connection#receive()
     * @see AbstractConnection#receivePacket(boolean)
     * @see AbstractConnection#sendAck(KtnDatagram, boolean)
     */
    @Override
    public String receive() throws ConnectException, IOException, InterruptedException {
    	if (state != State.ESTABLISHED){
            throw new IllegalStateException("Not in ESTABLISHED state.");
    	}
    	KtnDatagram ktn = null;
    	try{
    		try {
    			Thread.sleep(100);
    		} catch (InterruptedException ex) {
    			ex.printStackTrace();
    		}
    		ktn = receivePacket(false);
    	}catch (EOFException e){
    		throw e;
    	}catch (Exception e){
    		System.err.println("Error in receivePacket(false): " + e);
    	}
    	
    	if (veryValid(ktn) && ktn.getFlag() == Flag.NONE) {
    		//Perhaps add something with getSeq_nr() - however, its implemented in veryValid
            lastValidPacketReceived = ktn;
            try {
                sendAck(ktn, false);
            } catch (IOException e) { 
                throw new IOException("Unable to send ACK");
            }
            return (String)ktn.getPayload();
        }
        // Duplicate, try again
    	System.err.println("Duplicate packet, sending Ack on lastValidPacketReceived");
        try {
            sendAck(lastValidPacketReceived, false);
        } catch (IOException e) { 
            throw new IOException("Unable to send ACK");
        }
        return receive();
    }

    /**
     * Close the connection.
     * 
     * @see Connection#close()
     */
    public void close() throws IOException {
    	if (state != State.ESTABLISHED){
            throw new IllegalStateException("Can't disconnect when not connected.");
    	}
    	/*
    	 * Receiving disconnect request: Send FIN_ACK 
    	 * Sending disconnect request: Send FIN
    	 */
        if (disconnectRequest != null) {
            // Received FIN, send FIN_ACK
            try {
            	try {
            		Thread.sleep(100);
            	} catch (InterruptedException ex) { 
            		ex.printStackTrace();
            	}
                sendAck(disconnectRequest, false);
            } catch (Exception e) {
            	e.printStackTrace();
            }
            state = State.CLOSE_WAIT;
            //Make sure that ACK and FIN dont crash
            try { Thread.sleep(100); } catch (InterruptedException e) {  }
            //Send FIN
            KtnDatagram fin = constructInternalPacket(Flag.FIN), fin_ack = null;
            try {
                simplySendPacket(fin);
                state = State.LAST_ACK;
            } catch (Exception e) {
            	e.printStackTrace();
            }
            //Receive FIN_ACK
            int temp = 10; //Don't want an infinite loop
            while(!veryValid(fin_ack) && temp-->0){
            	try {
                    fin_ack = receivePacket(true);
                } catch (Exception e) {
                	e.printStackTrace();
                }
            }
            if (veryValid(fin_ack)) {
                state = State.CLOSED;
            }else{
            	state = State.CLOSED;
            	throw new IOException("Connection timeout on termination.");
            }
        } else {
            // Send FIN
        	try {
				Thread.sleep(500);
			} catch (InterruptedException e1) {
				e1.printStackTrace();
			}
            KtnDatagram fin = constructInternalPacket(Flag.FIN), fin_ack = null;
            try {
                simplySendPacket(fin);
                state = State.FIN_WAIT_1;
            } catch (Exception e) {
            	throw new RuntimeException("Could not send FIN", e);
            }
            //Receive FIN_ACK
            int temp = 10; //Avoiding infinite loops
            while(!veryValid(fin_ack) && temp-->0){
                fin_ack = receiveAck();
            }
            //FIN_ACK received?
            if (!veryValid(fin_ack)){
                throw new IOException("Could not close connection; did not receive FIN_ACK");
            }
            state = State.FIN_WAIT_2;

            // Receive FIN
            fin = null;
            temp=10; //In case FIN_ACK received on 9th try or something
            while(!veryValid(fin) && temp-->0){
		        try{
		        	fin = receivePacket(true);
                } catch (Exception f){
                	f.printStackTrace();
                }
            }
            //Is FIN valid?
            if (!veryValid(fin)){
            	throw new IOException("Failed to close connection; never received final FIN");
            }
            //Send FIN_ACK
            try {
            	try {
            		Thread.sleep(100);
            	} catch (InterruptedException e) {
            		e.printStackTrace();
            	}
                sendAck(fin, false);
            } catch (IOException e) { 
            	e.printStackTrace();
            }
            //TIME_WAIT skipped
            state = State.CLOSED;
        }
    }

    /**
     * Test a packet for transmission errors. This function should only called
     * with data or ACK packets in the ESTABLISHED state.
     * 
     * @param packet
     *            Packet to test.
     * @return true if packet is free of errors, false otherwise.
     */
    protected boolean isValid(KtnDatagram packet) {
    	return (packet != null && packet.getChecksum() == packet.calculateChecksum());
    }
    
    /*
     * Hjelpekode for � sjekke om en pakke virkelig er gyldig
     * Benytter seg av at pakker har forskjellige krav for � v�re godkjent
     * etter hvilken state de er i.
     */
    private boolean veryValid(KtnDatagram packet) {
        if (!isValid(packet)) return false;
        switch (state) {
        	case CLOSED: return false;
        	
        	case LISTEN: return 		(packet.getFlag() == Flag.SYN);
        	
        	case SYN_SENT: return 		(packet.getFlag() == Flag.SYN_ACK && packet.getSrc_addr().equals(remoteAddress));
        	
        	case SYN_RCVD: return 		(packet.getFlag() == Flag.ACK && packet.getSrc_addr().equals(remoteAddress) 
        								&& packet.getSrc_port() == remotePort);
        	
        	case ESTABLISHED: return 	((packet.getFlag() == Flag.NONE || packet.getFlag() == Flag.ACK || packet.getFlag() == Flag.FIN) 
        								&&  packet.getSrc_addr().equals(remoteAddress) && packet.getSrc_port() == remotePort
        								&& (packet.getFlag() == Flag.ACK || packet.getSeq_nr() > lastValidPacketReceived.getSeq_nr()));
        	
        	case FIN_WAIT_1: return 	(packet.getFlag() == Flag.ACK && packet.getSrc_addr().equals(remoteAddress) 
        								&& packet.getSrc_port() == remotePort);
        	
        	case CLOSE_WAIT: return 	(packet.getFlag() == Flag.FIN && packet.getSrc_addr().equals(remoteAddress) 
        								&& packet.getSrc_port() == remotePort);
        	
        	case LAST_ACK:	return 		(packet.getFlag() == Flag.ACK && packet.getSrc_addr().equals(remoteAddress) 
        								&& packet.getSrc_port() == remotePort);
        	
        	case FIN_WAIT_2: return 	(packet.getFlag() == Flag.FIN && packet.getSrc_addr().equals(remoteAddress) 
        								&& packet.getSrc_port() == remotePort);
        	
        	case TIME_WAIT: 
        }
        return false;
    }
    
    public boolean isConnected() {
    	return state == State.ESTABLISHED;
    }
}
/*
 *Melding fra Eirik:
 *Known stuff to do: Exception handling, muligens fjerne litt try/catch n�r vi veit at
 *stuff er stabilt i de omr�dene
 *Ellers, sjekk todo's
 */